package com.feng.service.impl;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.feng.entity.*;
import com.feng.entity.vo.*;
import com.feng.exception.SystemException;
import com.feng.mapper.AccountMapper;
import com.feng.mapper.ArticleMapper;
import com.feng.mapper.MenuMapper;
import com.feng.service.AccountArticleService;
import com.feng.service.AccountRoleService;
import com.feng.service.AccountService;
import com.feng.service.RoleService;
import com.feng.utils.Const;
import com.feng.utils.HttpCodeEnum;
import com.feng.utils.WebUtils;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {

    final
    JavaMailSender sender;

    final
    StringRedisTemplate redisTemplate;


    final
    PasswordEncoder passwordEncoder;

    final
    AccountMapper accountMapper;

    final
    MenuMapper menuMapper;


    final ArticleMapper articleMapper;

    final AccountRoleService accountRoleService;

    final
    AccountArticleService accountArticleService;

    final
    RoleService roleService;

    public AccountServiceImpl(JavaMailSender sender, StringRedisTemplate redisTemplate, PasswordEncoder passwordEncoder,
                              AccountMapper accountMapper, MenuMapper menuMapper, ArticleMapper articleMapper,
                              AccountRoleService accountRoleService, RoleService roleService, AccountArticleService accountArticleService) {
        this.sender = sender;
        this.redisTemplate = redisTemplate;
        this.passwordEncoder = passwordEncoder;
        this.accountMapper = accountMapper;
        this.menuMapper = menuMapper;
        this.articleMapper = articleMapper;
        this.accountRoleService = accountRoleService;
        this.roleService = roleService;
        this.accountArticleService = accountArticleService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        if (!StringUtils.hasText(username)) throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Account::getUsername, username);
        Account account = accountMapper.selectOne(wrapper);
        if (Objects.isNull(account)) throw new SystemException(HttpCodeEnum.LOGIN_ERROR);
        if (account.getStatus() == Const.ONE) throw new SystemException(HttpCodeEnum.ACCOUNT_ALREADY_DISABLED);
        if (account.getType() == Const.ZERO) {
            List<String> permission = menuMapper.getPermission(account.getId());
            return new User(account, permission);
        }
        return new User(account, null);
    }

    @Transactional
    public RestResponse register(AccountRegisterVo registerVo) {
        if (!StringUtils.hasText(registerVo.getNickname()))
            throw new SystemException(HttpCodeEnum.NICKNAME_NOT_NULL);
        if (!StringUtils.hasText(registerVo.getUsername()))
            throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        if (!StringUtils.hasText(registerVo.getPassword()))
            throw new SystemException(HttpCodeEnum.PASSWORD_NOT_NULL);
        if (usernameExist(registerVo.getUsername()))
            throw new SystemException(HttpCodeEnum.EMAIL_EXIST);
        if (nikeNameExist(registerVo.getNickname()))
            throw new SystemException(HttpCodeEnum.NICKNAME_EXIST);
        String code = redisTemplate.opsForValue().get(Const.EMAIL_CODE + registerVo.getUsername());
        if (code == null) throw new SystemException(HttpCodeEnum.EMAIL_OR_CODE_ERROR);
        if (!code.equals(registerVo.getCheckCode())) throw new SystemException(HttpCodeEnum.EMAIL_OR_CODE_ERROR);
        String uuid = UUID.randomUUID().toString();
        Account account = new Account();
        account.setNickname(registerVo.getNickname());
        account.setUsername(registerVo.getUsername());
        account.setId(uuid);
        account.setPassword(passwordEncoder.encode(registerVo.getPassword()));
        this.save(account);
        accountRoleService.save(new AccountRole(uuid, 2L));
        redisTemplate.delete(Const.EMAIL_CODE + account.getUsername());
        return RestResponse.success();
    }

    @Override
    public RestResponse sendEmail(String username, HttpServletRequest request) {
        if (!StringUtils.hasText(username)) throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        if (!username.endsWith(".com")) throw new SystemException(HttpCodeEnum.EMAIL_ERROR);
        if (usernameExist(username)) throw new SystemException(HttpCodeEnum.EMAIL_EXIST);
        String ip = request.getRemoteAddr();
        synchronized (ip.intern()) {
            String userIp = redisTemplate.opsForValue().get(Const.EMAIL_CODE_IP + ip);
            if (userIp != null) throw new SystemException(HttpCodeEnum.EMAIL_SEND_FREQUENT);
            else {
                redisTemplate.opsForValue().set(Const.EMAIL_CODE_IP + ip, "1", 59, TimeUnit.SECONDS);
            }
            Random random = new Random();
            Integer code = random.nextInt(900000) + 100000;
            SimpleMailMessage message = new SimpleMailMessage();
            message.setSubject("相逢向您发来注册验证码");
            message.setText("您的验证码是: " + code + "有效时间为10分钟，请注意填写；如不是本人操作，无需理会。");
            message.setTo(username);
            message.setFrom("m17633137954@163.com");
            try {
                sender.send(message);
            } catch (Exception e) {
                redisTemplate.delete(Const.EMAIL_CODE_IP + ip);
                throw new SystemException(HttpCodeEnum.EMAIL_ERROR);
            }
            redisTemplate.opsForValue().set(Const.EMAIL_CODE + username, code.toString(), 10, TimeUnit.MINUTES);
            return RestResponse.success();
        }
    }

    @Override
    public RestResponse forgotCode(String username, HttpServletRequest request) {
        if (!StringUtils.hasText(username)) throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        if (!username.endsWith(".com")) throw new SystemException(HttpCodeEnum.EMAIL_ERROR);
        if (!usernameExist(username)) throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        String ip = request.getRemoteAddr();
        synchronized (ip.intern()) {
            String userIp = redisTemplate.opsForValue().get(Const.FORGOT_CODE_IP + ip);
            if (userIp != null) throw new SystemException(HttpCodeEnum.EMAIL_SEND_FREQUENT);
            else {
                redisTemplate.opsForValue().set(Const.FORGOT_CODE_IP + ip, "1", 59, TimeUnit.SECONDS);
            }
            Random random = new Random();
            Integer code = random.nextInt(900000) + 100000;
            SimpleMailMessage message = new SimpleMailMessage();
            message.setSubject("相逢向您发来密码重置验证码");
            message.setText("您的验证码是: " + code + "有效时间为10分钟，请注意填写；如不是本人操作，无需理会。");
            message.setTo(username);
            message.setFrom("m17633137954@163.com");
            try {
                sender.send(message);
            } catch (Exception e) {
                redisTemplate.delete(Const.FORGOT_CODE_IP + ip);
                throw new SystemException(HttpCodeEnum.EMAIL_ERROR);
            }
            redisTemplate.opsForValue().set(Const.FORGOT_CODE + username, code.toString(), 10, TimeUnit.MINUTES);
            return RestResponse.success();
        }
    }

    @Override
    public RestResponse forgot(AccountForgotVo forgotVo) {
        if (!StringUtils.hasText(forgotVo.getUsername()))
            throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        String code = redisTemplate.opsForValue().get(Const.FORGOT_CODE + forgotVo.getUsername());
        if (code == null) throw new SystemException(HttpCodeEnum.EMAIL_OR_CODE_ERROR);
        if (!code.equals(forgotVo.getCheckCode())) throw new SystemException(HttpCodeEnum.EMAIL_OR_CODE_ERROR);
        redisTemplate.delete(Const.FORGOT_CODE + forgotVo.getUsername());
        redisTemplate.opsForValue().set(Const.FORGOT_EMAIL + forgotVo.getUsername(), "1", 10, TimeUnit.MINUTES);
        return RestResponse.success();
    }

    @Override
    public RestResponse changePassword(AccountForgotVo forgotVo) {
        if (!StringUtils.hasText(forgotVo.getUsername())) throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        if (!StringUtils.hasText(forgotVo.getPassword())) throw new SystemException(HttpCodeEnum.PASSWORD_NOT_NULL);
        Boolean key = redisTemplate.hasKey(Const.FORGOT_EMAIL + forgotVo.getUsername());
        if (!key) throw new SystemException(HttpCodeEnum.EMAIL_NOT_EXIST);
        LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Account::getUsername, forgotVo.getUsername());
        Account account = this.getOne(wrapper);
        account.setPassword(passwordEncoder.encode(forgotVo.getPassword()));
        this.updateById(account);
        redisTemplate.delete(Const.FORGOT_EMAIL + forgotVo.getUsername());
        return RestResponse.success();
    }

    @Override
    public RestResponse getAccountAvatar() {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String id = user.getAccount().getId();
        Account account = getById(id);
        AccountInfoVo accountInfoVo = new AccountInfoVo(account.getAvatar(), account.getNickname());
        return RestResponse.success(accountInfoVo);
    }

    @Override
    public RestResponse getAccountAndArticleById(String id) {
        Account account = accountMapper.getAccountById(id);
        List<Article> articles = articleMapper.selectArticleByAccountId(account.getId());
        AccountAndArticleVo accountAndArticle = new AccountAndArticleVo(account);
        List<ArticleListVo> articleListVos = new ArrayList<>();
        articles.stream()
                .forEach(article -> {
                    String viewCount = (String) redisTemplate.opsForHash().get(Const.REDIS_VIEW_COUNT_KET, article.getId());
                    articleListVos.add(new ArticleListVo(article, Long.valueOf(viewCount)));

                });
        accountAndArticle.setArticleVos(articleListVos);
        accountAndArticle.setCount(articleListVos.size());
        return RestResponse.success(accountAndArticle);
    }

    @Override
    public RestResponse getList(AccountListVo accountList) {
        LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(StringUtils.hasText(accountList.getNickname()), Account::getNickname, accountList.getNickname())
                .eq(StringUtils.hasText(accountList.getUsername()), Account::getUsername, accountList.getUsername())
                .eq(Objects.nonNull(accountList.getStatus()), Account::getStatus, accountList.getStatus());
        Page page = new Page(accountList.getCurrent(), accountList.getSize());
        page(page, wrapper);
        List<Account> accounts = page.getRecords();
        List<AccountListRespVo> accountListRespVos = new ArrayList<>();
        accounts.stream()
                .forEach(account -> {
                    accountListRespVos.add(new AccountListRespVo(account.getId(), account.getNickname(),
                            account.getUsername(), account.getSex(), account.getStatus().toString(), account.getCreateTime()));
                });
        ArticleListPageVo respVo = new ArticleListPageVo(accountListRespVos, page.getTotal());
        return RestResponse.success(respVo);
    }

    @Override
    public RestResponse updateAccountStatus(UpdateStatusReqVo updateStatus) {
        Account account = new Account();
        account.setId(updateStatus.getId());
        account.setStatus(updateStatus.getStatus());
        updateById(account);
        return RestResponse.success();
    }

    @Transactional
    @Override
    public RestResponse addAccount(AccountAddVo accountAdd) {
        if (!StringUtils.hasText(accountAdd.getNickname()))
            throw new SystemException(HttpCodeEnum.NICKNAME_NOT_NULL);
        if (!StringUtils.hasText(accountAdd.getUsername()))
            throw new SystemException(HttpCodeEnum.EMAIL_NOT_NULL);
        if (!StringUtils.hasText(accountAdd.getPassword()))
            throw new SystemException(HttpCodeEnum.PASSWORD_NOT_NULL);
        if (usernameExist(accountAdd.getUsername()))
            throw new SystemException(HttpCodeEnum.EMAIL_EXIST);
        if (nikeNameExist(accountAdd.getNickname()))
            throw new SystemException(HttpCodeEnum.NICKNAME_EXIST);
        String uuid = UUID.randomUUID().toString();
        Account account = new Account();
        account.setNickname(accountAdd.getNickname());
        account.setUsername(accountAdd.getUsername());
        account.setId(uuid);
        account.setPassword(passwordEncoder.encode(accountAdd.getPassword()));
        account.setStatus(accountAdd.getStatus());
        if (Objects.nonNull(accountAdd.getSex())) account.setSex(accountAdd.getSex());
        this.save(account);
        if (Objects.nonNull(accountAdd.getRoleId()))
            accountRoleService.save(new AccountRole(uuid, accountAdd.getRoleId()));
        else accountRoleService.save(new AccountRole(uuid, 2L));
        redisTemplate.delete(Const.EMAIL_CODE + account.getUsername());
        return RestResponse.success();
    }

    @Override
    public RestResponse getAccountById(String id) {
        Account account = getById(id);
        LambdaQueryWrapper<AccountRole> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AccountRole::getAccountId, account.getId());
        AccountRole accountRole = accountRoleService.getOne(wrapper);
        Role role = null;
        AccountAddVo accountAddVo = null;
        if (accountRole != null) {
            role = roleService.getById(accountRole.getRoleId());
            accountAddVo = new AccountAddVo(account.getId(), account.getNickname(), account.getUsername(),
                    account.getPassword(), account.getSex(), account.getStatus(), role.getId(), role.getRoleName());
        } else {
            accountAddVo = new AccountAddVo(account.getId(), account.getNickname(), account.getUsername(),
                    account.getPassword(), account.getSex(), account.getStatus(), null, null);
        }
        return RestResponse.success(accountAddVo);
    }

    @Transactional
    @Override
    public RestResponse updateAccount(AccountAddVo accountAdd) {
        Account account = new Account();
        account.setId(accountAdd.getId());
        account.setNickname(accountAdd.getNickname());
        account.setUsername(accountAdd.getUsername());
        account.setSex(accountAdd.getSex());
        account.setStatus(accountAdd.getStatus());
        updateById(account);
        LambdaUpdateWrapper<AccountRole> wrapper = new LambdaUpdateWrapper<>();
        if (Objects.nonNull(accountAdd.getRoleId())) {
            wrapper.eq(AccountRole::getAccountId, accountAdd.getId());
            accountRoleService.remove(wrapper);
            accountRoleService.save(new AccountRole(accountAdd.getId(), accountAdd.getRoleId()));
        }
        return RestResponse.success();
    }

    @Transactional
    @Override
    public RestResponse delAccount(String id) {
        removeById(id);
        LambdaQueryWrapper<AccountArticle> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AccountArticle::getAccountId, id);
        List<AccountArticle> accountArticles = accountArticleService.getBaseMapper().selectList(wrapper);
        if (accountArticles.size() != 0) {
            List<String> articleIds = accountArticles.stream()
                    .map(accountArticle -> accountArticle.getArticleId())
                    .collect(Collectors.toList());
            articleMapper.deleteByIds(articleIds);
        }
        return RestResponse.success();
    }

    @Transactional
    @Override
    public RestResponse delBatchAccount(List<String> ids) {
        removeBatchByIds(ids);
        LambdaQueryWrapper<AccountArticle> wrapper = new LambdaQueryWrapper<>();
        List<List<String>> articleListIds = new ArrayList<>();
        ids.stream()
                .forEach(id -> {
                    wrapper.eq(AccountArticle::getAccountId, id);
                    List<AccountArticle> accountArticles = accountArticleService.getBaseMapper().selectList(wrapper);
                    if (accountArticles.size() != 0) {
                        List<String> articleIds = accountArticles.stream()
                                .map(accountArticle -> accountArticle.getArticleId())
                                .collect(Collectors.toList());
                        articleListIds.add(articleIds);
                        articleMapper.deleteByIds(articleIds);
                    }
                    wrapper.clear();
                });
        articleListIds.stream()
                .forEach(articleIds -> {
                    articleMapper.deleteByIds(articleIds);
                });
        return RestResponse.success();
    }

    @Override
    public void export(HttpServletResponse response) {
        try (ServletOutputStream outputStream = response.getOutputStream()){
            WebUtils.excelResponseHeader(response, "用户");
            List<Account> accounts = list();
            List<AccountExportVo> exportVo = new ArrayList<>();
            LambdaQueryWrapper<AccountRole> wrapper = new LambdaQueryWrapper<>();
            accounts.forEach(account -> {
                wrapper.eq(AccountRole::getAccountId, account.getId());
                AccountRole role = accountRoleService.getOne(wrapper);
                exportVo.add(new AccountExportVo(account.getId(), account.getNickname(),
                        account.getUsername(), account.getSex(), account.getCreateTime(), account.getStatus(), role.getRoleId()));
                wrapper.clear();
            });
            EasyExcel.write(outputStream, AccountExportVo.class)
                    .autoCloseStream(Boolean.FALSE).sheet("用户导出")
                    .doWrite(exportVo);
        } catch (Exception e) {
            response.reset();
            WebUtils.renderString(response, RestResponse.failure(500, "用户导出出现了错误").asJsonString());
        }
    }

    @Override
    public RestResponse getUserInfo() {
        User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String id = user.getAccount().getId();
        Account account = getById(id);
        UserInfoVo userInfoVo = new UserInfoVo();
        BeanUtils.copyProperties(account, userInfoVo);
        return RestResponse.success(userInfoVo);
    }

    @Override
    public RestResponse updateAccountOneself(UserInfoUpdateVo userInfo) {
        User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if(!user.getAccount().getId().equals(userInfo.getId()))
            throw new SystemException(HttpCodeEnum.ACCOUNT_NO__PERMISSION);
        Account account = new Account();
        BeanUtils.copyProperties(userInfo, account);
        updateById(account);
        return RestResponse.success();
    }

    private boolean usernameExist(String username) {
        LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper();
        wrapper.eq(Account::getUsername, username);
        long count = count(wrapper);
        return count > 0;
    }

    private boolean nikeNameExist(String nickName) {
        LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper();
        wrapper.eq(Account::getNickname, nickName);
        long count = count(wrapper);
        return count > 0;
    }
}
